﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Restful.Net.Reflection
{
    /// <summary>
    /// Contains information about a method that will service a rest call.
    /// Does also service the actual invocation of the method.
    /// </summary>
    public class RestMethodInfo
    {
        /// <summary>
        /// The reflection information about the method this class holds information about.
        /// </summary>
        private readonly MethodInfo methodInfo;

        /// <summary>
        /// The Uri Template that is being used to match this method with.
        /// </summary>
        private readonly UriTemplate template;

        /// <summary>
        /// The list of parameters this method takes.
        /// </summary>
        private List<ParamInfo> parameters;

        /// <summary>
        /// Initializes a new instance of the <see cref="RestMethodInfo"/> class.
        /// </summary>
        /// <param name="methodInfo">The method info.</param>
        /// <param name="template">The template.</param>
        public RestMethodInfo(MethodInfo methodInfo, UriTemplate template)
        {
            this.methodInfo = methodInfo;
            this.template = template;
            Init();
        }

        /// <summary>
        /// Gets the uri template this method matches.
        /// </summary>
        public UriTemplate Template
        {
            get
            {
                return template;
            }
        }

        /// <summary>
        /// Gets the method that can be invoked.
        /// Can also be the begin method of an asynchronous method.
        /// </summary>
        public MethodInfo Method
        {
            get
            {
                return methodInfo;
            }
        }


        /// <summary>
        /// Gets the type of the declaring class this method is a member of.
        /// </summary>
        public Type DeclaringType
        {
            get
            {
                return methodInfo.DeclaringType;
            }
        }

        /// <summary>
        /// Gets the end method. Will be null if the method is not an asynchronous method.
        /// </summary>
        /// <value>The end method.</value>
        public MethodInfo EndMethod
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets a value indicating whether this method is an asynchronous method.
        /// </summary>
        public bool IsAsync
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets a value incating whether this method require authentication or not.
        /// </summary>
        public bool RequiresAuthentication
        {
            get;
            private set;
        }

        public RestMethodAttribute RestAttribute
        {
            get;
            set;
        }

        /// <summary>
        /// Initializes this instance, doing all the needed reflection work, to help speed up the processing run time, when invoking the method.
        /// </summary>
        private void Init()
        {
            RestMethodAttribute restMethodAttribute = Method.GetCustomAttribute<RestMethodAttribute>();

            if (restMethodAttribute != null)
            {
                RestAttribute = restMethodAttribute;
                RequiresAuthentication = restMethodAttribute.RequiresAuthentication;
            }

            ParameterInfo[] parameterInfos = Method.GetParameters();

            parameters = new List<ParamInfo>();
            foreach (ParameterInfo info in parameterInfos.OrderBy(pi => pi.Position))
            {
                Parameters.Add(new ParamInfo(info.Name, info.ParameterType, IsBuiltIn(info.ParameterType), info.DefaultValue));
            }

            if (!Method.ReturnType.IsAssignableFrom(typeof(IAsyncResult)))
            {
                if (Method.ReturnType.IsAssignableFrom(typeof(void)))
                {
                    IsVoid = true;
                }

                return;
            }

            // Async method, assume BeginXX, EndXX pattern
            // Begin verify

            IsAsync = true;

            if (!Method.Name.StartsWith("Begin"))
            {
                throw new InvalidOperationException(
                    "Asynchronous method should start with the prefix BeginXX where XX is the actual method name");
            }

            // Removes Begin from the end of the method
            string baseName = Method.Name.Substring(5);
            string endMethodName = string.Concat("End", baseName);
            MethodInfo endMethod = Method.DeclaringType.GetMethod(endMethodName);

            if (endMethod == null)
            {
                throw new InvalidOperationException(
                    "Asynchronous method should have an end method with the prefix EndXX where XX is the actual method name, paired with the BeginXX");
            }

            EndMethod = endMethod;

            if (EndMethod.ReturnType.IsAssignableFrom(typeof(void)))
            {
                IsVoid = true;
            }

            ParameterInfo[] endParameters = endMethod.GetParameters();
            if (endParameters.Count() != 1)
            {
                throw new InvalidOperationException("End method should have exactly one parameter");
            }

            if (!endParameters[0].ParameterType.IsAssignableFrom(typeof(IAsyncResult)))
            {
                throw new InvalidOperationException("End methods parameter should be of type IAsyncResult");
            }
        }

        public bool IsVoid
        {
            get;
            private set;
        }

        public List<ParamInfo> Parameters
        {
            get
            {
                return parameters;
            }
        }

        public IAsyncResult BeginInvoke(object instance, IRestContext context, AsyncCallback callback, object state)
        {
            object[] invokeParameters = GetParameters(context.Request);
            RestContext.Current = context;

            if (!IsAsync)
            {
                throw new InvalidOperationException(string.Format("Cannot invoke method :{0} asynchronously, since its not an asynchronous method", Method.Name));
            }

            invokeParameters[invokeParameters.Length] = state;
            invokeParameters[invokeParameters.Length - 1] = callback;

            return (IAsyncResult)Method.Invoke(instance, invokeParameters);
        }


        public object EndInvoke(object instance, IAsyncResult result)
        {
            return EndMethod.Invoke(instance, new object[] { result });
        }

        public object Invoke(object instance, IRestContext context)
        {
            object[] invokeParameters = GetParameters(context.Request);

            RestContext.Current = context;
            if (IsAsync)
            {
                invokeParameters[invokeParameters.Length] = null;
                invokeParameters[invokeParameters.Length - 1] = null;
                object result = Method.Invoke(instance, invokeParameters);
                return EndMethod.Invoke(instance, new[] { result });
            }

            return Method.Invoke(instance, invokeParameters);
        }

        private static bool IsBuiltIn(Type t)
        {
            return t.IsAssignableFrom(typeof(string)) || t.IsValueType;
        }

        private object[] GetParameters(IRequest request)
        {
            int x = 0;
            object[] invokeParams = new object[Parameters.Count];
            foreach (ParamInfo pi in Parameters)
            {
                if (pi.ParamType.IsAssignableFrom(typeof(AsyncCallback)))
                {
                    break;
                }

                if (pi.IsBuiltIn)
                {
                    // Should be available in the request variables
                    string objectValue = request.RestVariables[pi.Name];
                    if (objectValue != null)
                    {
                        invokeParams[x++] = Convert.ChangeType(objectValue, pi.ParamType);
                    }
                    else
                    {
                        invokeParams[x++] = request.GetRequestBody(pi.ParamType);
                    }
                }
                else
                {
                    invokeParams[x++] = request.GetRequestBody(pi.ParamType);
                }
            }

            return invokeParams;
        }
    }
}